package org.ouwh.fw.util.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Stack;

public final class XMLParser {
	private Node root;
	private Node current;
	private InputStream in = null;
	private byte[] buffer = new byte[10240];
	private Stack<String> stack = new Stack<>();
	private String encoding = "GBK";
	private int lineNum = 1;

	public static String replaceAllAmpForXML(String xmlStr) {
		String[] xmlStrGroup = xmlStr.split("&");
		String str = xmlStr;
		if (xmlStrGroup.length > 1) {
			str = xmlStrGroup[0];
			for (int i = 1; i < xmlStrGroup.length; i++) {
				xmlStrGroup[i] = "&" + xmlStrGroup[i];
				str = str + replaceOneAmpForXML(xmlStrGroup[i]);
			}
			if (xmlStr.endsWith("&")) {
				str = str + replaceOneAmpForXML("&");
			}
		} else {
			if (xmlStr.endsWith("&")) {
				str = str + "amp;";
			}
		}

		return str;
	}

	public static String replaceOneAmpForXML(String xmlStr) {
		String xmlStrS = xmlStr;
		int idx = 0;
		while (idx < xmlStrS.length()) {
			int i = xmlStrS.indexOf("&", idx);
			if (i == -1) {
				return xmlStrS;
			}
			if (xmlStrS.startsWith("&gt;", i) || xmlStrS.startsWith("&lt;", i) || xmlStrS.startsWith("&quot;", i)
					|| xmlStrS.startsWith("&apos;", i) || xmlStrS.startsWith("&amp;", i)) {
				idx = ++i;
				continue;
			}
			xmlStrS = xmlStrS.replaceFirst("&", "&amp;");
			idx = ++i;
		}
		return xmlStrS;
	}

	public Node parse(File file) throws Exception {
		FileInputStream in = null;
		try {
			in = new FileInputStream(file);
			return parse(in);
		} finally {
			try {
				in.close();
			} catch (Exception e) {
			}
		}
	}

	public Node parse(InputStream in) throws Exception {
		this.in = in;
		handleHeader();
		while (root == null) {
			readBlock('<');
			handleBlock();
		}
		while (stack.size() > 0) {
			handleBlock();
		}
		if (root != null) {
			root.setEncoding(encoding);
		}
		return root;
	}

	private void handleHeader() throws Exception {
		String block;
		do {
			readBlock('<');
			block = readBlock('>');
			if (block.startsWith("!--")) {
				if (!block.endsWith("-->")) {
					skipRem();
				}
			} else {
				break;
			}
		} while (true);
		int pos1 = block.indexOf("encoding=\"");
		int pos2 = block.indexOf("\"", pos1 + 10);
		if ((pos1 > 0) && (block.startsWith("?"))) {
			encoding = block.substring(pos1 + 10, pos2);
			if (encoding != null && encoding.length() != 0 && encoding.equalsIgnoreCase("GB2312")) {
				encoding = "GBK";
			}
		} else {
			handleElement(block);
		}
	}

	private void handleBlock() throws Exception {
		String block = readBlock('>').trim();
		//
		if (block.startsWith("!--")) {
			if (!block.endsWith("-->")) {
				skipRem();
			}
			if (stack.size() > 0)
				readBlock('<');
			return;
		}
		// CDATA
		if (block.startsWith("![CDATA[")) {
			int posCDataEnd = -1;
			while ((posCDataEnd = block.indexOf("]]>")) < 0) {
				block += readBlock('<').trim();
			}
			if (!block.endsWith("<"))
				readBlock('<');
			block = block.substring(8, posCDataEnd);
			startText(block);
			return;
		}
		handleElement(block);
	}

	private void handleElement(String element) throws Exception {
		String block = element;
		// be pare
		while (countComma(block) % 2 > 0) {
			// not use StringBuffer
			block += readBlock('>').trim();
		}
		int posA = block.indexOf("/");
		int posB = block.indexOf(' ');
		if (posA > 0) {
			// posA > 0 indicate no text
			block = block.substring(0, block.length() - 1);
			String name = startElement(block);
			if (block.charAt(block.length() - 1) == '/')
				endElement(name);
			if (stack.size() > 0)
				readBlock('<');
		} else if (posA == 0) {
			// posA = 0 element ends
			block = block.substring(1, posB > 0 ? posB : block.length() - 1);
			endElement(block);
			if (stack.size() > 0)
				readBlock('<');
		} else {
			// posA < 0, has text
			block = block.substring(0, block.length() - 1);
			startElement(block);
			// attr uppon, next to handle text
			block = readBlock('<').trim();
			if (block.length() > 1) {
				block = block.substring(0, block.indexOf('<')).trim();
				startText(block);
			}
			// CDATA for next readBlock
		}
	}

	private String startElement(String typeAndAttr) {
		try {
			int tabPos = typeAndAttr.indexOf("\t");
			int blankPos = typeAndAttr.indexOf(" ");
			if ((tabPos >= 0) && ((tabPos < blankPos) || (blankPos < 0))) {
				blankPos = tabPos;
			}
			String name = typeAndAttr;
			Attribute[] attrs = null;
			if (blankPos >= 0) {
				name = typeAndAttr.substring(0, blankPos);
				String strAttrs = typeAndAttr.substring(blankPos).trim();
				ArrayList<Attribute> a = new ArrayList<>();
				while (true) {
					int eqPos = strAttrs.indexOf("=");
					if (eqPos < 0)
						break;
					String attrName = strAttrs.substring(0, eqPos).trim();
					strAttrs = strAttrs.substring(eqPos + 1).trim();
					int fstPos = strAttrs.indexOf("\"");
					int scdPos = strAttrs.indexOf("\"", fstPos + 1);
					String attrValue = strAttrs.substring(fstPos + 1, scdPos);
					attrValue = attrValue.replaceAll("&amp;", "&");
					// attrValue = replaceAllAmpForXML(attrValue);
					a.add(new Attribute(attrName, attrValue));
					strAttrs = strAttrs.substring(scdPos);
					if (strAttrs.length() > 1)
						strAttrs = strAttrs.substring(1).trim();
				}
				attrs = (Attribute[]) a.toArray(new Attribute[0]);
			}
			if (name.endsWith("/")) {
				name = name.substring(0, name.length() - 1);
			}
			name = name.trim();
			Node node = new Node(name, attrs);
			node.setLineNumber(lineNum);
			if (current == null)
				root = node;
			else
				current.addChild(node);
			current = node;
			stack.push(name);
			return name;
		} catch (Exception e) {
			throw new RuntimeException("line: " + lineNum + ", xml content: " + typeAndAttr);
		}
	}

	private void startText(String value) {
		current.setXmlValue(value);
	}

	private void endElement(String name) throws Exception {
		String startName = (String) stack.pop();
		if (!startName.equals(name)) {
			throw new Exception("Difference between startTag and endTag." + startName + "]");
		}
		current = current.getFather();
	}

	private String readBlock(char c) throws Exception {
		int off = 0;
		int chr = -1;
		while (off < 10240 - 1) {
			chr = in.read();
			if (chr == '\n')
				lineNum++;
			if (chr == c) {
				buffer[off++] = (byte) chr;
				return new String(buffer, 0, off, encoding);
			} else if (chr == -1) {
				throw new Exception("XML Content not integral!");
			} else {
				buffer[off++] = (byte) chr;
			}
		}
		throw new Exception("The Content is not XML Format.");
	}

	private void skipRem() throws Exception {
		int chr = -1;
		while (true) {
			chr = in.read();
			if (chr == '\n')
				lineNum++;
			if (chr == '-') {
				chr = in.read();
				if (chr == '\n')
					lineNum++;
				if (chr == '-') {
					chr = in.read();
					if (chr == '\n')
						lineNum++;
					if (chr == '>')
						return;
				}

			} else if (chr == -1) {
				throw new Exception("XML remark not integral.");
			}
		}
	}

	private int countComma(String src) {
		int count = 0;
		int pos = src.indexOf('"');
		while (pos >= 0) {
			count++;
			pos = src.indexOf('"', pos + 1);
		}
		return count;
	}
}
